import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;


public class SelectSplitPoint {
	private ArrayList<Data> examples;
	private HashMap<Integer,ArrayList<Double>> perfectPointMap;
	
	/*
	 *  Constructor to do some initialization
	 */
	public SelectSplitPoint(String filename){
		/*
		 * This HashMap is to store the best Attribute-SplitPoints pairs, attribute we use Integer to represent
		 */
		perfectPointMap = new HashMap<Integer,ArrayList<Double>>(); 
		/*
		 * examples stores the Data read from the TXT file
		 */
		examples = new FileRead(filename).getDataList();
	}
	
	/*
	 *  Empty constructor
	 */
	public SelectSplitPoint(){
		perfectPointMap = new HashMap<Integer,ArrayList<Double>>();
	}
	
	/**
	 * This method is to get the best converted examples, we choose the split points for each continue-valued attribute each time
	 * there are four continue-valued attribute, so it calls splitPoint() four times
	 * @return
	 */
	public ArrayList<Data> getPerfectExamples(){
		ArrayList<Data> vacation = splitPoint(examples,InitialAttribute.Vacation);
		ArrayList<Data> eCredit = splitPoint(vacation,InitialAttribute.eCredit);
		ArrayList<Data> salary = splitPoint(eCredit,InitialAttribute.Salary);
		ArrayList<Data> proportion = splitPoint(salary,InitialAttribute.Property);
		return proportion;
	}
	
	
	/**
	 * This method is to calculate every possible split points and pass to another method to choose the best one
	 * @param attribute
	 * @return
	 */
	public ArrayList<Data> splitPoint(ArrayList<Data> setOfExamples,int attribute){
		double min = 0;
		double max = 0;
		int k = 3;
		ArrayList<Double> points = new ArrayList<Double>();
		
		/*
		 *  Based on different attribute, we calculate the minimum and maximum for them 
		 */
		if(attribute == InitialAttribute.Vacation){
			Collections.sort(setOfExamples, new SortByVacation());	
			min = Double.parseDouble(setOfExamples.get(0).getAttribute(attribute));
			max = Double.parseDouble(setOfExamples.get(setOfExamples.size()-1).getAttribute(attribute));
		}
		if(attribute == InitialAttribute.eCredit){
			Collections.sort(setOfExamples, new SortByeCredit());	
			min = Double.parseDouble(setOfExamples.get(0).getAttribute(attribute));
			max = Double.parseDouble(setOfExamples.get(setOfExamples.size()-1).getAttribute(attribute));
		}
		if(attribute == InitialAttribute.Salary){
			Collections.sort(setOfExamples, new SortBySalary());
			min = Double.parseDouble(setOfExamples.get(0).getAttribute(attribute));
			max = Double.parseDouble(setOfExamples.get(setOfExamples.size()-1).getAttribute(attribute));
		}
		if(attribute == InitialAttribute.Property){
			Collections.sort(setOfExamples, new SortByProperty());
			min = Double.parseDouble(setOfExamples.get(0).getAttribute(attribute));
			max = Double.parseDouble(setOfExamples.get(setOfExamples.size()-1).getAttribute(attribute));
		}
		
		/*
		 * This way is to go through all the examples, and choose N-1 possible split point(get the average number between two attribute)
		 * this part is used to get the best one split point, but after the test, it can not get a acceptable accuracy
		for(int i=0;i<setOfExamples.size()-1;i++){
			points.add((Double.parseDouble(setOfExamples.get(i).getAttribute(attribute))+Double.parseDouble(setOfExamples.get(i+1).getAttribute(attribute)))/2);
		}
		*/
		
		/*
		 *  This way is from http://research.microsoft.com/pubs/65569/splits.pdf, to choose the split point more efficiently
		 *  K here mean K branches for continue-valued attribute node
		 *  In this task, we choose 3 split points to build the DT
		 */
		for(int i=1;i<=k;i++){
			points.add(min + i*((max-min)/(k+1)));		
		}
		
		perfectPointMap.put(attribute, points);
		/*
		 *  The parameter points.get(0),points.get(1),points.get(2) means we have three split points
		 */
		return convert(setOfExamples,points.get(0),points.get(1),points.get(2),attribute);
	}
		
	/**
	 * This method to calculate the information gain based on the examples with different split point
	 * We will choose the best one which has the maximum information gain
	 * @param points
	 * @param attribute
	 * @return
	 */
	public ArrayList<Data> getConvertedExamples(ArrayList<Data> setOfExamples,ArrayList<Double> points, int attribute){
		InformationGain information = new InformationGain();
		HashMap<Double,ArrayList<Double>> gainsSplitPoints = new HashMap<Double,ArrayList<Double>>();
		HashMap<ArrayList<Double>,ArrayList<Data>> examplesPoints = new HashMap<ArrayList<Double>,ArrayList<Data>>();
		ArrayList<Double> gains = new ArrayList<Double>();
		double maxGain;
		ArrayList<Double> perfectPoints;
		ArrayList<Data> perfectExamples;
		
		/*
		 * Because we use ID3 and chosen only one split point before, the accuracy is to low
		 * So, this part is to choose best two split point, but the O(n^2) is not good, if run this
		 * part we need to expand the heap space by using -Xmx1024m
		 */
		for(int i=0;i<points.size()-1;i++){
			double point1 = points.get(i);
			for(int m=i+1;m<points.size();m++){
				double point2 = points.get(m);
			  ArrayList<Data> convertedExamples = convert(setOfExamples,point1,point2,attribute);
			  double informationGain = information.getGain(convertedExamples, attribute); // This is the part to calculate the information gain
			  gains.add(informationGain);	
			  
			  ArrayList<Double> splitPoints = new ArrayList<Double>();
			  splitPoints.add(point1);
			  splitPoints.add(point2);
			  /*
			   * Add the result to different HashMap, in order to get the data what we want
			   */
			  gainsSplitPoints.put(informationGain, splitPoints);
			  examplesPoints.put(splitPoints, convertedExamples);
			}
		}
		
		Collections.sort(gains); // Sort all the information gains, and get the maximum one	
		maxGain = gains.get(gains.size()-1);
		perfectPoints = gainsSplitPoints.get(maxGain);// Get the split points which is matched to the maximum IG
		perfectExamples = examplesPoints.get(perfectPoints);// Get the perfect converted examples
		
		perfectPointMap.put(attribute, perfectPoints);	
		return perfectExamples;
	}
	
	/**
	 * This method is to format the original examples, we will use < > = to 
	 * replace the real numbers in the example
	 * 
	 * This method is for two split points
	 * @param splitPoint
	 * @param attribute
	 * @return
	 */
	public ArrayList<Data> convert(ArrayList<Data> setOfExamples,double splitPoint1,double splitPoint2,int attribute){
		ArrayList<Data> converted = new ArrayList<Data>();	
		
		/*
		 * If we use setOfExamples to update the value of attribute, it will change the only one Object in the heap,
		 * so we declare a new ArrayList<Data> to transfer the data here
		 */
		for(int i=0;i<setOfExamples.size();i++){
			Data data = new Data();
			for(int inner=0;inner<setOfExamples.get(i).size();inner++){
			    data.addAttribute(setOfExamples.get(i).getAttribute(inner));
			}
			converted.add(data);
			data =null;
		}
		
		/*
		 * Convert the data in ArrayList<Data>, we separate the data into three categories
		 * < means if the number is less than split point1, it will be replaced by <
		 * > means if the number is bigger than split point2, it will be replaced by >
		 * = means if the number is between split point1 and split point2, it will be replaced by =
		 */
		for(int i = 0;i<converted.size();i++){
			try{
			    double value = Double.parseDouble(converted.get(i).getAttribute(attribute));
			    if(value<splitPoint1){
			    	converted.get(i).updateAttribute(attribute, "<");
			    } else if(value>splitPoint2)converted.get(i).updateAttribute(attribute, ">");
			    else converted.get(i).updateAttribute(attribute, "=");
			} catch (Exception e){
				System.out.println("Attribute is not number");
			}		
		}
		return converted;
	}
	
	
	/**
	 * This method is to format the original examples, we will use < > = to 
	 * replace the real numbers in the example
	 * 
	 * This method is for three split points
	 */
	public ArrayList<Data> convert(ArrayList<Data> setOfExamples,double splitPoint1,double splitPoint2,double splitPoint3,int attribute){
		ArrayList<Data> converted = new ArrayList<Data>();	
		
		
		for(int i=0;i<setOfExamples.size();i++){
			Data data = new Data();
			for(int inner=0;inner<setOfExamples.get(i).size();inner++){
			    data.addAttribute(setOfExamples.get(i).getAttribute(inner));
			}
			converted.add(data);
			data =null;
		}
		
		/*
		 * Convert the data in ArrayList<Data>, we separate the data into three categories
		 * << means if the number is less than split point1, it will be replaced by <<
		 * <> means if the number is bigger than split point1 and less than split point2, it will be replaced by <>
		 * >< means if the number is bigger than split point2 and less than split point3, it will be replaced by ><
		 * >> means if the number is bigger than split point3, it will be replaced by >>
		 */
		for(int i = 0;i<converted.size();i++){
			try{
			    double value = Double.parseDouble(converted.get(i).getAttribute(attribute));
			    if(value<splitPoint1){
			    	converted.get(i).updateAttribute(attribute, "<<");
			    } else if(value<splitPoint2&&value>splitPoint1)converted.get(i).updateAttribute(attribute, "<>");
			    else if(value>splitPoint2&&value<splitPoint3) converted.get(i).updateAttribute(attribute, "><");
			    else converted.get(i).updateAttribute(attribute, ">>");
			} catch (Exception e){
				System.out.println("Attribute is not number");
			}		
		}
		return converted;
	}
	
	/**
	 * This method is to get the perfect split points
	 * @return
	 */
	public ArrayList<Double> getPerfectPoints(int attribute){
		return perfectPointMap.get(attribute);
	}
	
}
